#pragma once

#include <cstdint>
#include <unordered_map>
#include <any>

#include "coroutine.h"

namespace coroutine {

/// Type of call ID
using CallID = std::uint64_t;
constexpr CallID INVALID_CALL_ID = CallID(0);
constexpr CallID CALL_ID_START = CallID(1);
class Call;

/// Call's result
enum class CallResult {
    FAIL = 0, ///< Failure
    OK = 1, ///< Success
    TIMEOUT, ///< Timeout
    NO_RESULT, ///< Called but no result
};

/// The type of call
enum class CallType {
    NONE = 0,
    NO_ARG = 1, // No argument
    NO_RET, // No return value
    ARG_RET, // argument & return value
    NO_ARG_RET, // No argument and return value
    NO_ARG_TIMEOUT, // No argument with timeout
    NO_RET_TIMEOUT, // No return value with timeout
    ARG_RET_TIMEOUT, // argument & return value with timeout
    NO_ARG_RET_TIMEOUT, // No argument and return value with timeout
};

struct ProtocolData_ {
    char* raw;
    std::size_t size;
};

/// 
/// template <typename T>
/// inline ProtocolData fertilize(T&)
/// User should implements 'fertilize' method for user defined type

template <typename T>
inline ProtocolData_ fertilize(T&& msg) {
    return {reinterpret_cast<char*>(&msg), sizeof(T)};
}

template <>
inline ProtocolData_ fertilize(std::nullptr_t&&) {
    return {nullptr, 0};
}

template <>
inline ProtocolData_ fertilize(char* const&& msg) {
    if (!msg) {
        return {nullptr, 0};
    }
    return {msg, std::string(msg).size()};
}

template <>
inline ProtocolData_ fertilize(const char*&& msg) {
    if (!msg) {
        return {nullptr, 0};
    }
    return {const_cast<char*>(msg), std::string(msg).size()};
}

constexpr std::int32_t ERROR_NOT_SET = -1;
constexpr std::int32_t ERROR_OK = 1;
constexpr std::int32_t ERROR_FAIL = 2;

/// Coroutine protocol
class CoroProtocol {
    CallID id_ {INVALID_CALL_ID}; ///< Call ID
    CallType type_ {CallType::NONE}; ///< The type of call
    friend class Call;
    std::int32_t errorID_ { ERROR_NOT_SET }; ///< Error ID

public:
    /// Returns call' ID for this call
    CallID getID() const;
    /// Call and wait for completion, yield
    /// @return @see CallResult
    CallResult call();
    /// Call and wait for completion, yield
    /// @param timeout The maximum expired time in millionsecond
    /// @return @see CallResult
    CallResult call(std::time_t timeout);
    /// Call and wait for completion, yield
    /// @param msg The message need to send
    /// @return @see CallResult
    template <typename T>
    CallResult call(T&& msg);
    /// Call and wait for completion, yield
    /// @param msg The message need to send
    /// @param timeout The maximum expired time in millionsecond
    /// @return @see CallResult
    template <typename T>
    CallResult call(T&& msg, std::time_t timeout);
    /// Call and wait for result, yield
    /// @param retval The message should return back
    /// @return @see CallResult
    template <typename OutputT>
    CallResult callNoParam(OutputT& retval);
    /// Call and wait for result, yield
    /// @param retval The message should return back
    /// @param timeout The maximum expired time in millionsecond
    /// @return @see CallResult
    template <typename OutputT>
    CallResult callNoParam(OutputT& retval, std::time_t timeout);
    /// Call and wait for result, yield
    /// @param msg The message need to send
    /// @param retval The message should return back
    /// @return @see CallResult
    template <typename InputT, typename OutputT>
    CallResult call(InputT&& msg, OutputT& retval);
    /// Call and wait for result, yield
    /// @param msg The message need to send
    /// @param retval The message should return back
    /// @param timeout The maximum expired time in millionsecond
    /// @return @see CallResult
    template <typename InputT, typename OutputT>
    CallResult call(InputT&& msg, OutputT& retval, std::time_t timeout);
    /// Wakeup with return message
    template <typename T>
    void receive(T&& msg, std::int32_t error = ERROR_OK);
    /// Wakeup but no returned message
    void error(std::int32_t error = ERROR_FAIL);
    /// Wakeup but no returned message
    void success();

private:
    /// Get remote call's error code
    std::int32_t getError();
    /// Send message to remote peer
    /// @param data data in bytes
    /// @param size data length
    /// @retval true or false
    bool send(const char* data, std::size_t size);
    /// User defined low level send method
    /// @param id remote call ID
    /// @param data data in bytes
    /// @param size data length
    /// @retval true or false
    virtual bool send_(CallID id, const char* data, std::size_t size) = 0;
};

/// Do call and yield
class Call {
    friend class CoroProtocol;
    friend class CoroSched_;

    CallID rcID_ {CALL_ID_START};
    using CallMap = std::unordered_map<CallID, coroutine::CoroID>;
    CallMap waitCalls_; ///< Pending call
    /// Get newest call ID
    CallID getCurrentCallID() const {
        return rcID_;
    }
    // Clear pending calls
    void clear();
    /// Send data to peer, Call in child coroutine
    /// @param sender user defined sender
    /// @param data user data
    /// @param size user data size
    /// @return result @see CallResult
    CallResult call(CoroProtocol* sender, const char* data, std::size_t size);
    /// Send data to peer, Call in child coroutine
    /// @param sender user defined sender
    /// @param data user data
    /// @param size user data size
    /// @param timeout Maximum waiting time in millionsecond
    /// @return result @see CallResult
    CallResult call(CoroProtocol* sender, const char* data, std::size_t size, std::time_t timeout);
    /// Send data to peer, Call in child coroutine
    /// @param sender user defined sender
    /// @param data user data
    /// @param size user data size
    /// @param [IN OUT] retval result from peer
    /// @return result @see CallResult
    template <typename OutputT>
    CallResult call(CoroProtocol* sender, const char* data, std::size_t size, OutputT& retval) {
        if (coro_is_main()) {
            return CallResult::FAIL;
        }
        if (!sender->send(data, size)) {
            return CallResult::FAIL;
        }
        waitCalls_.emplace(rcID_++, coro_id());
        auto mail = coro_pop_wait();
        if (sender->getError() != ERROR_OK) {
            return CallResult::FAIL;
        }
        if (mail) {
            retval = std::any_cast<OutputT>(mail);
        } else {
            return CallResult::NO_RESULT;
        }
        return CallResult::OK;
    }
    /// Send data to peer, Call in child coroutine
    /// @param sender user defined sender
    /// @param data user data
    /// @param size user data size
    /// @param [IN OUT] retval result from peer
    /// @param timeout Maximum waiting time in millionsecond
    /// @return result @see CallResult
    template <typename OutputT>
    CallResult call(CoroProtocol* sender, const char* data, std::size_t size, OutputT& retval,
        std::time_t timeout) {
        if (coro_is_main()) {
            return CallResult::FAIL;
        }
        if (!sender->send(data, size)) {
            return CallResult::FAIL;
        }
        waitCalls_.emplace(rcID_++, coro_id());
        auto mail = coro_pop_wait(timeout);
        if (!mail) {
            if (sender->getError() == ERROR_NOT_SET) {
                return CallResult::TIMEOUT;
            } else if (sender->getError() != ERROR_OK) {
                return CallResult::FAIL;
            }
        }
        if (mail) {
            retval = std::any_cast<OutputT>(mail);
        } else {
            return CallResult::TIMEOUT;
        }
        return CallResult::OK;
    }
    /// Receive data from peer, Call in main coroutine
    /// @param rcID call ID
    /// @param msg received user defined message
    template <typename T>
    void call_return(CallID rcID, T&& msg) {
        if (!coro_is_main()) {
            return;
        }
        auto it = waitCalls_.find(rcID);
        if (it == waitCalls_.end()) {
            return;
        }
        auto coroID = it->second;
        waitCalls_.erase(it);
        coro_resume(coroID, msg);
    }
    /// Receive data from peer, Call in main coroutine
    /// @param rcID call ID
    void call_return(CallID rcID);
};

template <typename T>
CallResult CoroProtocol::call(T&& msg) {
    type_ = CallType::NO_RET;
    auto data = fertilize(msg);
    return rc().call(this, data.raw, data.size);
}

template <typename T>
CallResult CoroProtocol::call(T&& msg, std::time_t timeout) {
    type_ = CallType::NO_RET_TIMEOUT;
    auto data = fertilize(msg);
    return rc().call(this, data.raw, data.size, timeout);
}

template <typename OutputT>
CallResult CoroProtocol::callNoParam(OutputT& retval) {
    type_ = CallType::NO_ARG;
    return rc().call(this, nullptr, 0, retval);
}

template <typename OutputT>
CallResult CoroProtocol::callNoParam(OutputT& retval, std::time_t timeout) {
    type_ = CallType::NO_ARG_TIMEOUT;
    return rc().call(this, nullptr, 0, retval, timeout);
}

template <typename InputT, typename OutputT>
CallResult CoroProtocol::call(InputT&& msg, OutputT& retval) {
    type_ = CallType::ARG_RET;
    auto data = fertilize(msg);
    return rc().call(this, data.raw, data.size, retval);
}

template <typename InputT, typename OutputT>
CallResult CoroProtocol::call(InputT&& msg, OutputT& retval, std::time_t timeout) {
    type_ = CallType::ARG_RET_TIMEOUT;
    auto data = fertilize(msg);
    return rc().call(this, data.raw, data.size, retval, timeout);
}

template <typename T>
void CoroProtocol::receive(T&& msg, std::int32_t error) {
    if (error) {
        errorID_ = error;
    }
    rc().call_return(id_, msg);
}

}
